//-----------------------------------------------------------------------------------------------
// Author: Andre Van Der Merwe <dart@pobox.com>
// Project: .net Compound File (http://dotnetpcf.sourceforge.net)
//
// Modifications
//    2001-02-18: (AVDM) Added CurrentBlockSize propery
//    2001-02-18: (AVDM) Added FillUnusedBlocks()
// 
// Copyright (c) Andre Van Der Merwe 2000-2001
// Licence: BSD - see the licence.txt file or visit the project homepage
//-----------------------------------------------------------------------------------------------


namespace dotnetpcf.CompoundFile
{
   using System;
   using System.Diagnostics;
   using System.Collections;
   using System.Data;
   using System.IO;

   /// <summary>
   /// The system level class that provides the basic compound file functionality.
   /// This class is intended for internal use only.
   /// </summary>
   /// <remarks>
   /// Design goals
   ///    1) Hierachial file structure in single file (compound file)
   ///    2) Demand loading of streams. i.e. Do not need to load entire file just to read a single stream
   ///    3) Usable from non-dotnet languages, the main consequense of this being that no serialization was used
   ///    4) The compound file implementation should not add a big space/speed overhead
   /// 
   /// The physical file is split into blocks of SysInfo.BLOCK_BYTE_SIZE (currently 2048) bytes each.
   /// 
   /// block 0 - Always the compound file header (CompoundFileHeader) which contains vital
   /// system information, including the block indexes of the other system blocks. 
   /// 
   /// For this version the 4 system blocks have fixed starting positions. This may change in future
   /// versions and there positions must be read from the header. The other system blocks are listed
   /// below along with a short description, see the classes that implement these blocks for more information
   /// 
   /// block 1 - The allocation table. Each block in the file is described by a single entry in the alloc table
   /// block 2 - The file table. Files can span multiple blocks, files belong to dirs
   /// block 3 - The dir table. Dirs are hierachial.
   /// block 4 - The names table. All file/dir names are stored in the names table which is saved to a single file
   /// 
   /// 
   /// Order of loading the compound file
   ///    1) The compound file header is loaded
   ///    2) The allocation table is loaded
   ///    3) The file table is loaded. Once the file table is loaded FSFilterStreams can use used to load
   ///        all other data
   ///    4) The dir table is loaded
   ///    5) The names table is loaded
   /// 
   /// </remarks>
   public class CompoundFileSys
   {
      public CompoundFileSys()
      {
         Debug.Assert(  CompoundFileHeader.SIZE <= SysInfo.BLOCK_BYTE_SIZE,  "Block size is too small"  );
         Debug.Assert(  dotnetpcf.CompoundFile.AllocTable.AllocTableHeader.SIZE <= SysInfo.BLOCK_BYTE_SIZE,  "Block size is too small"  );
         Debug.Assert(  dotnetpcf.CompoundFile.FileTable.FileTableEntry.SIZE <= SysInfo.BLOCK_BYTE_SIZE,  "Block size is too small"  );

         m_Names = new CFNames(  this  );
         m_DirTable = new DirTable(  this  );
         m_FileTable = new FileTable(  this  );
         m_AllocTable = new AllocTable(  this  );
      }

      private CFNames m_Names;
      private DirTable m_DirTable;
      private FileTable m_FileTable;
      private AllocTable m_AllocTable;
      private Stream m_CompoundFileStream = null;
      private CompoundFileHeader m_Header = new CompoundFileHeader();


      public virtual CompoundFileResult OpenFile(  string sFileName  )
      {
         try
         {
            m_CompoundFileStream = new FileStream(  sFileName,  FileMode.OpenOrCreate,  FileAccess.ReadWrite  );

            if(  m_CompoundFileStream.Length > 1  )
               return LoadFromExistingStream(  m_CompoundFileStream  );
            else
               return InitNewFile();
         }
         catch(  CompoundFileException e  )
         {
            Debug.Write(  "CompoundFileException in OpenFile: cfr= " + e.m_CompoundFileResult.ToString()  );
            //!
            throw;
            //!
            //return e.m_CompoundFileResult;
         }
         catch(  Exception e  )
         {
            Debug.Write(  "exception in OpenFile: " + e.Message  );
            //!
            throw;
            //!
            //return CompoundFileResult.cfrUnhandledException;
         }
      }

      public virtual CompoundFileResult OpenFile(Stream stream)
      {
          try
          {
              m_CompoundFileStream = stream;

              if (m_CompoundFileStream.Length > 1)
                  return LoadFromExistingStream(m_CompoundFileStream);
              else
                  return InitNewFile();
          }
          catch (CompoundFileException e)
          {
              Debug.Write("CompoundFileException in OpenFile: cfr= " + e.m_CompoundFileResult.ToString());
              //!
              throw;
              //!
              //return e.m_CompoundFileResult;
          }
          catch (Exception e)
          {
              Debug.Write("exception in OpenFile: " + e.Message);
              //!
              throw;
              //!
              //return CompoundFileResult.cfrUnhandledException;
          }
      }
      private CompoundFileResult LoadFromExistingStream(  Stream strm  )
      {
         //----Load------------------------

            //Load the compound file header
         m_CompoundFileStream.Position = 0;
         m_Header.LoadFromStream(  m_CompoundFileStream  );

            //Load the allocation table
         m_CompoundFileStream.Position = m_Header.m_iAllocTableIndex * SysInfo.BLOCK_BYTE_SIZE;
         m_AllocTable.LoadFromStream(  m_CompoundFileStream  );

         ////////////////////////////////////////////////////////////////////////////////////
         // Load the file table. Some trickery is required to do this... A file table entry
         //  is required to create the virtual file stream (FSFilterStream) from which the
         //  file table is to be loaded. However, the file table is obviously not loaded yet.
         //  So the physical position of the file table's file table entry is stored in the 
         //  compound file header. Also the file table's length is incorrect because of the 
         //  way the file table is saved (i.e. the length is updated while saving, but the
         //  file table entry, where there length, is stored has already been updated by 
         //  the time the lenght is known). So the file table length must be read from the 
         //  header
         //
         //  Thus to load the file table
         //   1) Get and seek to the file table entry pos
         //   2) Load the file table entry
         //   3) Create a virtual file stream
         //   4) Load
         //   5) Get the file table stream length from the header
         ////////////////////////////////////////////////////////////////////////////////////
         FileTable.FileTableEntry fent = new FileTable.FileTableEntry(  m_FileTable  );
         m_CompoundFileStream.Position = m_Header.m_iPhysicalPosOfFileTable_FileTableEntry;
         fent.LoadFromStream(  m_CompoundFileStream  );
            //Retrieve the file table's stream length
         fent.m_iLength = m_Header.m_iVirtualSizeOfFileTable;
         FSFilterStream filter = new FSFilterStream(  m_CompoundFileStream,  fent  );
         m_FileTable.LoadFromStream(  filter  );

            //Load the directory table
         fent = m_FileTable.FindDirTableEntry();
         Debug.Assert(  null != fent,  "cant find dir table to load"  );
         filter = new FSFilterStream(  m_CompoundFileStream,  fent  );
         m_DirTable.LoadFromStream(  filter  );

            //Load the names
         fent = m_FileTable.FindNamesEntry();
         Debug.Assert(  null != fent,  "cant find names to load"  );
         filter = new FSFilterStream(  m_CompoundFileStream,  fent  );
         m_Names.LoadFromStream(  filter  );
         

         //----Check------------------------
         
         ///////////////
         // alloc table
         ///////////////
         AllocTable.AllocTableEntry ent = m_AllocTable.GetAllocTableEntryByID(  m_Header.m_iAllocTableIndex  );/*(AllocTable.AllocTableEntry)m_AllocTable.m_Entries[ m_Header.m_iAllocTableIndex ]*/;
         Debug.Assert(  null != ent  );
         Debug.Assert(  0 != ((uint)AllocTableFlags.efAllocTable & ent.m_iFlags),  "Alloc table - bad alloc table idx"  );
         if(  0 == ((uint)AllocTableFlags.efAllocTable & ent.m_iFlags)  )
            throw(  new CompoundFileException(  CompoundFileResult.cfrLoadCheckAllocTableFail  )  );

         fent = m_FileTable.FindAllocTableEntry();
         Debug.Assert(  null != fent  );
         Debug.Assert(  fent.m_iAllocTblIndex == m_Header.m_iAllocTableIndex  );
         if(  (null == fent) || (fent.m_iAllocTblIndex != m_Header.m_iAllocTableIndex))
            throw(  new CompoundFileException(  CompoundFileResult.cfrLoadCheckAllocTableFail  )  );


         //////////////
         // File table
         //////////////
         ent = m_AllocTable.GetAllocTableEntryByID(  m_Header.m_iFileTableIndex  );/*(AllocTable.AllocTableEntry)m_AllocTable.m_Entries[ m_Header.m_iFileTableIndex ]*/;
         Debug.Assert(  null != ent  );
         Debug.Assert(  0 != ((uint)AllocTableFlags.efFileTable & ent.m_iFlags),  "File table - bad alloc table idx"  );
         if(  0 == ((uint)AllocTableFlags.efFileTable & ent.m_iFlags)  )
            throw(  new CompoundFileException(  CompoundFileResult.cfrLoadCheckFileTableFail  )  );
         
         fent = m_FileTable.FindFileTableEntry();
         Debug.Assert(  null != fent  );
         Debug.Assert(  fent.m_iAllocTblIndex == m_Header.m_iFileTableIndex  );
         if(  (null == fent) || (fent.m_iAllocTblIndex != m_Header.m_iFileTableIndex))
            throw(  new CompoundFileException(  CompoundFileResult.cfrLoadCheckFileTableFail  )  );

         
         /////////////
         // Dir table
         /////////////
         ent = ent = m_AllocTable.GetAllocTableEntryByID(  m_Header.m_iDirTableIndex  ); /*(AllocTable.AllocTableEntry)m_AllocTable.m_Entries[ m_Header.m_iDirTableIndex ];*/
         Debug.Assert(  null != ent  );
         Debug.Assert(  0 != ((uint)AllocTableFlags.efDirTable & ent.m_iFlags),  "Dir table - bad alloc table idx"  );
         if(  0 == ((uint)AllocTableFlags.efDirTable & ent.m_iFlags)  )
            throw(  new CompoundFileException(  CompoundFileResult.cfrLoadCheckDirTableFail  )  );

         fent = m_FileTable.FindDirTableEntry();
         Debug.Assert(  null != fent  );
         Debug.Assert(  fent.m_iAllocTblIndex == m_Header.m_iDirTableIndex  );
         if(  (null == fent) || (fent.m_iAllocTblIndex != m_Header.m_iDirTableIndex))
            throw(  new CompoundFileException(  CompoundFileResult.cfrLoadCheckDirTableFail  )  );
         

         return CompoundFileResult.cfrOk;
      }


      /// <summary>
      /// Initialises a new stream as a compound file
      /// </summary>
      private CompoundFileResult InitNewFile()
      {
         //--------Alloc table----------------

         /////////////////////////////////////////////////
         //Add an alloc table entry for the header block
         /////////////////////////////////////////////////
         AllocTable.AllocTableEntry ent = new AllocTable.AllocTableEntry();
         ent.m_iID = 0;
         ent.m_iNext = SysInfo.UNDEFINED;
         ent.m_iFlags = (uint)(AllocTableFlags.efCFHeader | AllocTableFlags.efSystem);
         m_AllocTable.AddAllocTableEntry/*m_Entries.Add*/(  (uint)0,  ent  );

         //////////////////////////////////////////////////////
         // Add an alloc table entry for the alloc table block
         //////////////////////////////////////////////////////
         ent = new AllocTable.AllocTableEntry();
         ent.m_iID = m_Header.m_iAllocTableIndex;
         ent.m_iNext = SysInfo.UNDEFINED;
         ent.m_iFlags = (uint)(AllocTableFlags.efAllocTable | AllocTableFlags.efSystem);
         m_AllocTable.AddAllocTableEntry/*m_Entries.Add*/(  m_Header.m_iAllocTableIndex, ent  );

         /////////////////////////////////////////////////////
         // Add an alloc table entry for the file table block
         /////////////////////////////////////////////////////
         ent = new AllocTable.AllocTableEntry();
         ent.m_iID = m_Header.m_iFileTableIndex;
         ent.m_iNext = SysInfo.UNDEFINED;
         ent.m_iFlags = (uint)(AllocTableFlags.efFileTable | AllocTableFlags.efSystem);
         m_AllocTable.AddAllocTableEntry/*m_Entries.Add*/(  m_Header.m_iFileTableIndex,  ent  );

         ////////////////////////////////////////////////////
         // Add an alloc table entry for the dir table block
         ////////////////////////////////////////////////////
         ent = new AllocTable.AllocTableEntry();
         ent.m_iID = m_Header.m_iDirTableIndex;
         ent.m_iNext = SysInfo.UNDEFINED;
         ent.m_iFlags = (uint)(AllocTableFlags.efDirTable | AllocTableFlags.efSystem);
         m_AllocTable.AddAllocTableEntry/*m_Entries.Add*/(  m_Header.m_iDirTableIndex,  ent  );


         //--------File table----------------
         FileTable.FileTableEntry fent;

         ///////////////////////////////////////////////
         // Create a file table entry for the CF header
         ///////////////////////////////////////////////
         fent = new FileTable.FileTableEntry(  m_FileTable  );
         fent.m_iID = 0; //!
         fent.m_iAllocTblIndex = 0;
         fent.m_iFlags = (uint)(FileFlags.ffCFHeader | FileFlags.ffSystem);
         fent.m_iLength = CompoundFileHeader.SIZE;
         m_FileTable.AddNewFileTableEntry(  fent.m_iID,  fent  );


         /////////////////////////////////////////////////
         // Create a file table entry for the alloc table
         /////////////////////////////////////////////////
         fent = new FileTable.FileTableEntry(  m_FileTable  );
         fent.m_iID = 1; //!
         fent.m_iAllocTblIndex = m_Header.m_iAllocTableIndex;
         fent.m_iFlags = (uint)(FileFlags.ffAllocTable | FileFlags.ffSystem);
         fent.m_iLength = 0; //!
         m_FileTable.AddNewFileTableEntry(  fent.m_iID,  fent  );


         ////////////////////////////////////////////////
         // Create a file table entry for the file table
         ////////////////////////////////////////////////
         fent = new FileTable.FileTableEntry(  m_FileTable  );
         fent.m_iID = 2; //!
         fent.m_iAllocTblIndex = m_Header.m_iFileTableIndex;
         fent.m_iFlags = (uint)(FileFlags.ffFileTable | FileFlags.ffSystem);
         fent.m_iLength = 0; //!
         m_FileTable.AddNewFileTableEntry(  fent.m_iID,  fent  );


         ///////////////////////////////////////////////
         // Create a file table entry for the dir table
         ///////////////////////////////////////////////
         fent = new FileTable.FileTableEntry(  m_FileTable  );
         fent.m_iID = 3; //!
         fent.m_iAllocTblIndex = m_Header.m_iDirTableIndex;
         fent.m_iFlags = (uint)(FileFlags.ffDirTable | FileFlags.ffSystem);
         fent.m_iLength = 0; //!
         m_FileTable.AddNewFileTableEntry(  fent.m_iID,  fent  );
         

         //------------------------------------------------------
         // Other build in files can use the file/alloc table
         //------------------------------------------------------

            //Create the names file
         fent = m_FileTable.CreateNewFile(  (AllocTableFlags.efNames | AllocTableFlags.efSystem),  
                                            (FileFlags.ffNames | FileFlags.ffSystem),  
                                            SysInfo.FILENAME_NAMES  
                                         );

         m_Header.m_iNamesIndex = fent.m_iAllocTblIndex;

         return CompoundFileResult.cfrOk;
      }

      public virtual void Save()
      {
         FSFilterStream filter = null;
         FileTable.FileTableEntry fent = null;

         //////////////////
         // Save the names
         //////////////////
         fent = m_FileTable.FindNamesEntry();
         Debug.Assert(  null != fent,  "cant find names to save"  );
         filter = new FSFilterStream(  m_CompoundFileStream,  fent  );
         m_Names.SaveToStream(  filter  );
         filter.Close();


         //////////////////////
         // Save the dir table
         //////////////////////
         fent = m_FileTable.FindDirTableEntry();
         Debug.Assert(  null != fent,  "cant find dir table to save"  );
         filter = new FSFilterStream(  m_CompoundFileStream,  fent  );
         m_DirTable.SaveToStream(  filter  );
         filter.Close();


         ////////////////////////////////////////////////////////////////////////////////
         // Save the file table
         //  Must be saved just before the alloc table - Writing to files (eg dir table) 
         //  will change the length value in the file table entry. Thus the file table
         //  must be saved after anything that would cause a file table entry change.
         //  The alloc table does not use a FSFilterStream and thus need the length
         //  value from the file table entry
         ////////////////////////////////////////////////////////////////////////////////
         
            //There must be space for the file table block
         if(  m_CompoundFileStream.Length < (SysInfo.BLOCK_BYTE_SIZE * (m_Header.m_iFileTableIndex + 1))  )
         {
            m_CompoundFileStream.SetLength(  SysInfo.BLOCK_BYTE_SIZE * (m_Header.m_iFileTableIndex + 1)  );
            m_CompoundFileStream.Position = SysInfo.BLOCK_BYTE_SIZE * m_Header.m_iFileTableIndex;
         }


         fent = m_FileTable.FindFileTableEntry();
         filter = new FSFilterStream(  m_CompoundFileStream,  fent  );
         m_FileTable.SaveToStream(  filter  );
         filter.Close();
            //Save the file table stream's length
         m_Header.m_iVirtualSizeOfFileTable = fent.m_iLength;



         ////////////////////////////////////////////////////////////////
         // Save the alloc table
         //  Must be saved after anything that can cause changes to the 
         //  alloct table
         ////////////////////////////////////////////////////////////////

            //There must be space for the alloc table block
         if(  m_CompoundFileStream.Length < (SysInfo.BLOCK_BYTE_SIZE * (m_Header.m_iAllocTableIndex + 1))  )
         {
            m_CompoundFileStream.SetLength(  SysInfo.BLOCK_BYTE_SIZE * (m_Header.m_iAllocTableIndex + 1)  );
            m_CompoundFileStream.Position = SysInfo.BLOCK_BYTE_SIZE * m_Header.m_iAllocTableIndex;
         }

         m_CompoundFileStream.Position = SysInfo.BLOCK_BYTE_SIZE * m_Header.m_iAllocTableIndex;
         m_AllocTable.SaveToStream(  m_CompoundFileStream  );

         Debug.Assert(  !m_AllocTable.Modified,  "Alloc table has been modified durning/after save"  );


         ////////////////////////////////////////////////////////////////////
         // Save the compound file header
         //  Needs to be saved last as it is updated by the file table etc
         ////////////////////////////////////////////////////////////////////
         
            //There must be space for 1 whole block - the header
         if(  m_CompoundFileStream.Length < SysInfo.BLOCK_BYTE_SIZE  )
            m_CompoundFileStream.SetLength(  SysInfo.BLOCK_BYTE_SIZE  );

         m_CompoundFileStream.Position = 0;
         m_Header.SaveToStream(  m_CompoundFileStream  );

         Debug.Assert(  !m_AllocTable.Modified,  "Alloca table has been modified durning/after save"  );
      }
            

      public virtual void CloseFile()
      {
         if(  null == m_CompoundFileStream )
            return;

         m_CompoundFileStream.Close();
         m_AllocTable = null;
         m_CompoundFileStream = null;
      }


      /// <summary>
      /// Fills all unused blocks with a '*' character
      /// </summary>
      public virtual void FillUnusedBlocks()
      {
         uint iNumBlocks = (uint)(m_CompoundFileStream.Length / SysInfo.BLOCK_BYTE_SIZE + ((0 == m_CompoundFileStream.Length % SysInfo.BLOCK_BYTE_SIZE) ? 0 : 1));

         byte[] arFill = new byte[ SysInfo.BLOCK_BYTE_SIZE ];
         
         for(  uint i = 0;  i < SysInfo.BLOCK_BYTE_SIZE;  ++i  )
            arFill[ i ] = (byte)'*';

         long iPos = m_CompoundFileStream.Position;

         try
         {
            for(  uint i = 1;  i < iNumBlocks;  ++i  )
            {
               if(  null == m_AllocTable.GetAllocTableEntryByID(  i  )  )
               {
                  m_CompoundFileStream.Position = i * SysInfo.BLOCK_BYTE_SIZE;
                  m_CompoundFileStream.Write(  arFill,  0,  SysInfo.BLOCK_BYTE_SIZE  );
               }
            }
         }
         finally
         {
            m_CompoundFileStream.Position = iPos;
         }
      }

      public uint CurrentBlockSize
      {
         get{ return SysInfo.BLOCK_BYTE_SIZE; }
      }

      internal CFNames Names
      {
         get{ return m_Names; }
      }

      internal DirTable DirTable
      {
         get{ return m_DirTable; }
      }

      internal FileTable FileTable
      {
         get{ return m_FileTable; }
      }

      internal AllocTable AllocTable
      {
         get{ return m_AllocTable; }
      }

      internal Stream CompoundFileStream
      {
         get{ return m_CompoundFileStream; }
      }

      internal CompoundFileHeader Header
      {
         get{ return m_Header; }
      }
   }
}
